package hibatis.support;

import hibatis.EmptyMapper;
import org.apache.ibatis.reflection.ParamNameResolver;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.RowBounds;
import org.apache.ibatis.session.defaults.DefaultSqlSession;
import org.springframework.beans.BeanUtils;
import org.springframework.util.ReflectionUtils;

import java.beans.PropertyDescriptor;
import java.lang.annotation.Annotation;
import java.lang.reflect.*;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;

/**
 * Created by huangdachao on 2018/6/15 17:54.
 */
public class Reflection {

    public static String formatMethod(Class<?> mapperClass, Method method) {
        return mapperClass.getName() + "." + method.getName();
    }

    public static String formatMethod(Method method) {
        return method.getDeclaringClass().getName() + "." + method.getName();
    }

    public static String formatField(Field f) {
        return f.getDeclaringClass().getName() + "." + f.getName();
    }

    public static boolean isNumberZero(Object val) {
        return val != null && Number.class.isAssignableFrom(val.getClass()) && ((Number) val).floatValue() == 0;
    }

    @SuppressWarnings("unchecked")
    public static <T> T findAnnotation(Annotation[] annotations, Class<T> type) {
        for (int i = 0; i < annotations.length; i++) {
            if (annotations[i].annotationType() == type) {
                return (T) annotations[i];
            }
        }

        return null;
    }

    public static Class<?> getEntityClass(Class<?> mapperClass) {
        Type[] types = mapperClass.getGenericInterfaces();
        for (Type type : types) {
            if (type instanceof ParameterizedType) {
                ParameterizedType t = (ParameterizedType) type;
                if (EmptyMapper.class.isAssignableFrom((Class<?>) t.getRawType())) {
                    return (Class<?>) t.getActualTypeArguments()[0];
                }
            }
        }

        for (Type type : types) {
            if (type instanceof Class) {
                Class<?> clazz = getEntityClass((Class) type);
                if (clazz != null) {
                    return clazz;
                }
            }
        }
        return null;
    }

    public static Object getFieldValue(Object bean, Field field) {
        Map<String, PropertyDescriptor> properties = Arrays.stream(BeanUtils.getPropertyDescriptors(bean.getClass()))
            .collect(Collectors.toMap(PropertyDescriptor::getName, Function.identity()));
        PropertyDescriptor pd = properties.get(field.getName());
        if (pd == null) {
            return null;
        }

        try {
            Method m = pd.getReadMethod();
            if (m == null) {
                throw new ReflectiveOperationException("getter方法没找到");
            }
            return pd.getReadMethod().invoke(bean);
        } catch (ReflectiveOperationException e) {
            throw new RuntimeException("类" + bean.getClass().getCanonicalName() + "的属性" + field.getName() + "不可访问", e);
        }
    }

    public static List<Field> resolveFields(Class<?> type, boolean writable) {
        Map<String, PropertyDescriptor> properties = Arrays.stream(BeanUtils.getPropertyDescriptors(type))
            .collect(Collectors.toMap(PropertyDescriptor::getName, Function.identity()));

        final List<Field> fields = new ArrayList<>();
        ReflectionUtils.doWithFields(type, field -> {
            if (Modifier.isStatic(field.getModifiers()) || Modifier.isTransient(field.getModifiers())) {
                return;
            }

            PropertyDescriptor pd = properties.get(field.getName());
            if (pd != null && ((writable && pd.getWriteMethod() != null) || (!writable && pd.getReadMethod() != null))) {
                fields.add(field);
            }
        });

        return fields;
    }

    public static List<Member> resolveProperties(Class<?> type) {
        Map<String, PropertyDescriptor> properties = Arrays.stream(BeanUtils.getPropertyDescriptors(type))
                .collect(Collectors.toMap(PropertyDescriptor::getName, Function.identity()));

        final List<Member> fields = new ArrayList<>();
        ReflectionUtils.doWithFields(type, field -> {
            if (Modifier.isStatic(field.getModifiers()) || Modifier.isTransient(field.getModifiers())) {
                return;
            }

            PropertyDescriptor pd = properties.get(field.getName());
            properties.remove(field.getName());
            if (pd != null && pd.getReadMethod() != null) {
                fields.add(field);
            }
        });

        properties.values().forEach(p -> {
            Method m = p.getReadMethod();
            if (m != null) {
                fields.add(new PropertyMember(p.getName(), m));
            }
        });
        return fields;
    }

    public static Object getPropertyValue(Object target, Member m) {
        if (m instanceof Field) {
            return getFieldValue(target, (Field) m);
        } else if (m instanceof Method) {
            try {
                return ((Method) m).invoke(target);
            } catch (IllegalAccessException | InvocationTargetException ex) {
                throw new RuntimeException(ex);
            }
        } else if (m instanceof PropertyMember) {
            try {
                return (((PropertyMember) m).getMethod().invoke(target));
            } catch (IllegalAccessException | InvocationTargetException ex) {
                throw new RuntimeException(ex);
            }
        }
        return null;
    }

    public static Annotation[] getPropertyAnnotations(Member m) {
        if (m instanceof AccessibleObject) {
            return ((AccessibleObject) m).getAnnotations();
        } else if (m instanceof PropertyMember) {
            return ((PropertyMember) m).getMethod().getAnnotations();
        }
        return new Annotation[0];
    }

    public static <T extends Annotation> T getPropertyAnnotation(Member m, Class<T> type) {
        if (m instanceof AccessibleObject) {
            return ((AccessibleObject) m).getAnnotation(type);
        }
        return null;
    }

    @SuppressWarnings("unchecked")
    public static Object[] parseArgs(Configuration conf, Method method, Object parameterObject) {
        int argCount = method.getParameterCount();
        if (argCount > 0) {
            Type[] types = method.getGenericParameterTypes();
            for (Type t : types) {
                if (t == RowBounds.class) {
                    argCount -= 1;
                }
            }
        }
        if (argCount == 0) {
            return new Object[0];
        } else if (argCount == 1) {
            if (parameterObject instanceof DefaultSqlSession.StrictMap) {
                Map<String, Object> map = (Map<String, Object>) parameterObject;
                return new Object[]{map.values().iterator().next()};
            }
            return new Object[]{parameterObject};
        } else if (parameterObject instanceof Map) {
            @SuppressWarnings("unchecked")
            Map<String, Object> params = (Map<String, Object>) parameterObject;
            String[] argumentNames = new ParamNameResolver(conf, method).getNames();
            Object[] args = new Object[argumentNames.length];
            for (int i = 0; i < args.length; i++) {
                args[i] = params.get(argumentNames[i]);
            }
            return args;
        } else {
            throw new RuntimeException("参数无法解析：" + Reflection.formatMethod(method.getDeclaringClass(), method));
        }
    }

    public static Type resolveType(Type type, Type srcType, Class<?> declaringClass) {
        if (type instanceof TypeVariable) {
            return resolveTypeVar((TypeVariable<?>) type, srcType, declaringClass);
        } else if (type instanceof ParameterizedType) {
            return resolveParameterizedType((ParameterizedType) type, srcType, declaringClass);
        } else if (type instanceof GenericArrayType) {
            return resolveGenericArrayType((GenericArrayType) type, srcType, declaringClass);
        } else {
            return type;
        }
    }

    public static Type resolveGenericArrayType(GenericArrayType genericArrayType, Type srcType, Class<?> declaringClass) {
        Type componentType = genericArrayType.getGenericComponentType();
        Type resolvedComponentType = null;
        if (componentType instanceof TypeVariable) {
            resolvedComponentType = resolveTypeVar((TypeVariable<?>) componentType, srcType, declaringClass);
        } else if (componentType instanceof GenericArrayType) {
            resolvedComponentType = resolveGenericArrayType((GenericArrayType) componentType, srcType, declaringClass);
        } else if (componentType instanceof ParameterizedType) {
            resolvedComponentType = resolveParameterizedType((ParameterizedType) componentType, srcType, declaringClass);
        }
        if (resolvedComponentType instanceof Class) {
            return Array.newInstance((Class<?>) resolvedComponentType, 0).getClass();
        } else {
            return new GenericArrayTypeImpl(resolvedComponentType);
        }
    }

    public static ParameterizedType resolveParameterizedType(ParameterizedType parameterizedType, Type srcType, Class<?> declaringClass) {
        Class<?> rawType = (Class<?>) parameterizedType.getRawType();
        Type[] typeArgs = parameterizedType.getActualTypeArguments();
        Type[] args = new Type[typeArgs.length];
        for (int i = 0; i < typeArgs.length; i++) {
            if (typeArgs[i] instanceof TypeVariable) {
                args[i] = resolveTypeVar((TypeVariable<?>) typeArgs[i], srcType, declaringClass);
            } else if (typeArgs[i] instanceof ParameterizedType) {
                args[i] = resolveParameterizedType((ParameterizedType) typeArgs[i], srcType, declaringClass);
            } else if (typeArgs[i] instanceof WildcardType) {
                args[i] = resolveWildcardType((WildcardType) typeArgs[i], srcType, declaringClass);
            } else {
                args[i] = typeArgs[i];
            }
        }
        return new ParameterizedTypeImpl(rawType, null, args);
    }

    private static Type resolveWildcardType(WildcardType wildcardType, Type srcType, Class<?> declaringClass) {
        Type[] lowerBounds = resolveWildcardTypeBounds(wildcardType.getLowerBounds(), srcType, declaringClass);
        Type[] upperBounds = resolveWildcardTypeBounds(wildcardType.getUpperBounds(), srcType, declaringClass);
        return new WildcardTypeImpl(lowerBounds, upperBounds);
    }

    private static Type[] resolveWildcardTypeBounds(Type[] bounds, Type srcType, Class<?> declaringClass) {
        Type[] result = new Type[bounds.length];
        for (int i = 0; i < bounds.length; i++) {
            if (bounds[i] instanceof TypeVariable) {
                result[i] = resolveTypeVar((TypeVariable<?>) bounds[i], srcType, declaringClass);
            } else if (bounds[i] instanceof ParameterizedType) {
                result[i] = resolveParameterizedType((ParameterizedType) bounds[i], srcType, declaringClass);
            } else if (bounds[i] instanceof WildcardType) {
                result[i] = resolveWildcardType((WildcardType) bounds[i], srcType, declaringClass);
            } else {
                result[i] = bounds[i];
            }
        }
        return result;
    }

    public static Type resolveTypeVar(TypeVariable<?> typeVar, Type srcType, Class<?> declaringClass) {
        Type result = null;
        Class<?> clazz = null;
        if (srcType instanceof Class) {
            clazz = (Class<?>) srcType;
        } else if (srcType instanceof ParameterizedType) {
            ParameterizedType parameterizedType = (ParameterizedType) srcType;
            clazz = (Class<?>) parameterizedType.getRawType();
        } else {
            throw new IllegalArgumentException("The 2nd arg must be Class or ParameterizedType, but was: " + srcType.getClass());
        }

        if (clazz == declaringClass) {
            Type[] bounds = typeVar.getBounds();
            if (bounds.length > 0) {
                return bounds[0];
            }
            return Object.class;
        }

        Type superclass = clazz.getGenericSuperclass();
        result = scanSuperTypes(typeVar, srcType, declaringClass, clazz, superclass);
        if (result != null) {
            return result;
        }

        Type[] superInterfaces = clazz.getGenericInterfaces();
        for (Type superInterface : superInterfaces) {
            result = scanSuperTypes(typeVar, srcType, declaringClass, clazz, superInterface);
            if (result != null) {
                return result;
            }
        }
        return Object.class;
    }

    private static Type scanSuperTypes(TypeVariable<?> typeVar, Type srcType, Class<?> declaringClass, Class<?> clazz, Type superclass) {
        if (superclass instanceof ParameterizedType) {
            ParameterizedType parentAsType = (ParameterizedType) superclass;
            Class<?> parentAsClass = (Class<?>) parentAsType.getRawType();
            TypeVariable<?>[] parentTypeVars = parentAsClass.getTypeParameters();
            if (srcType instanceof ParameterizedType) {
                parentAsType = translateParentTypeVars((ParameterizedType) srcType, clazz, parentAsType);
            }
            if (declaringClass == parentAsClass) {
                for (int i = 0; i < parentTypeVars.length; i++) {
                    if (typeVar == parentTypeVars[i]) {
                        return parentAsType.getActualTypeArguments()[i];
                    }
                }
            }
            if (declaringClass.isAssignableFrom(parentAsClass)) {
                return resolveTypeVar(typeVar, parentAsType, declaringClass);
            }
        } else if (superclass instanceof Class && declaringClass.isAssignableFrom((Class<?>) superclass)) {
            return resolveTypeVar(typeVar, superclass, declaringClass);
        }
        return null;
    }

    private static ParameterizedType translateParentTypeVars(ParameterizedType srcType, Class<?> srcClass, ParameterizedType parentType) {
        Type[] parentTypeArgs = parentType.getActualTypeArguments();
        Type[] srcTypeArgs = srcType.getActualTypeArguments();
        TypeVariable<?>[] srcTypeVars = srcClass.getTypeParameters();
        Type[] newParentArgs = new Type[parentTypeArgs.length];
        boolean noChange = true;
        for (int i = 0; i < parentTypeArgs.length; i++) {
            if (parentTypeArgs[i] instanceof TypeVariable) {
                for (int j = 0; j < srcTypeVars.length; j++) {
                    if (srcTypeVars[j] == parentTypeArgs[i]) {
                        noChange = false;
                        newParentArgs[i] = srcTypeArgs[j];
                    }
                }
            } else {
                newParentArgs[i] = parentTypeArgs[i];
            }
        }
        return noChange ? parentType : new ParameterizedTypeImpl((Class<?>) parentType.getRawType(), null, newParentArgs);
    }

    static class ParameterizedTypeImpl implements ParameterizedType {
        private Class<?> rawType;

        private Type ownerType;

        private Type[] actualTypeArguments;

        ParameterizedTypeImpl(Class<?> rawType, Type ownerType, Type[] actualTypeArguments) {
            super();
            this.rawType = rawType;
            this.ownerType = ownerType;
            this.actualTypeArguments = actualTypeArguments;
        }

        @Override
        public Type[] getActualTypeArguments() {
            return actualTypeArguments;
        }

        @Override
        public Type getOwnerType() {
            return ownerType;
        }

        @Override
        public Type getRawType() {
            return rawType;
        }

        @Override
        public String toString() {
            return "ParameterizedTypeImpl [rawType=" + rawType + ", ownerType=" + ownerType + ", actualTypeArguments=" + Arrays.toString(actualTypeArguments) + "]";
        }
    }

    static class WildcardTypeImpl implements WildcardType {
        private Type[] lowerBounds;

        private Type[] upperBounds;

        WildcardTypeImpl(Type[] lowerBounds, Type[] upperBounds) {
            super();
            this.lowerBounds = lowerBounds;
            this.upperBounds = upperBounds;
        }

        @Override
        public Type[] getLowerBounds() {
            return lowerBounds;
        }

        @Override
        public Type[] getUpperBounds() {
            return upperBounds;
        }
    }

    static class GenericArrayTypeImpl implements GenericArrayType {
        private Type genericComponentType;

        GenericArrayTypeImpl(Type genericComponentType) {
            super();
            this.genericComponentType = genericComponentType;
        }

        @Override
        public Type getGenericComponentType() {
            return genericComponentType;
        }
    }
}
